package pro.cherish.smartfile.component;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.retry.RetryException;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import pro.cherish.smartfile.common.annotation.LogId;
import pro.cherish.smartfile.common.enums.biz.DataType;
import pro.cherish.smartfile.common.enums.biz.IconType;
import pro.cherish.smartfile.common.enums.biz.TaskStatusEnum;
import pro.cherish.smartfile.common.exception.BusinessException;
import pro.cherish.smartfile.common.response.Result;
import pro.cherish.smartfile.component.event.ChunkMergeEvent;
import pro.cherish.smartfile.config.properties.SysProperties;
import pro.cherish.smartfile.module.dto.TaskDto;
import pro.cherish.smartfile.module.entity.Attachment;
import pro.cherish.smartfile.module.entity.AttachmentChunkTemp;
import pro.cherish.smartfile.module.req.ChunkMd5Module;
import pro.cherish.smartfile.module.req.FileUploadReq;
import pro.cherish.smartfile.module.req.StartUploadReq;
import pro.cherish.smartfile.service.AttachmentChunkTempService;
import pro.cherish.smartfile.service.AttachmentService;
import pro.cherish.smartfile.util.Md5Util;
import reactor.core.publisher.Mono;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author 创建人：lys
 * @version 版本号：V1.0
 * <p>
 * ***************************修订记录************************************
 * 2020-12-04 lys 创建该类功能。
 * **********************************************************************
 * </p>
 * @ClassName 类名：FileManage
 * @Description 功能说明：
 * <p>
 *
 * </p>
 * **********************************************************************
 * @date 创建日期： 2020-12-04 lys
 */
@Component
@Slf4j
public class FileManager {

    @Autowired
    private SysProperties sysProperties;

    @Autowired
    private AttachmentService attachmentService;

    @Autowired
    private AttachmentChunkTempService attachmentChunkTempService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    /**
     * 合并
     *
     * @param fileUploadReq
     * @param filePart
     * @return
     */
    public void upload2(FileUploadReq fileUploadReq, FilePart filePart) throws BusinessException {
        String taskId = fileUploadReq.getIdentifier();
        Integer chunk = fileUploadReq.getChunkNumber();
        Integer currentChunkSize = fileUploadReq.getCurrentChunkSize();
        try {
            File tempFile;
            String tempFilePath = sysProperties.getTempRootPath(taskId) + File.separator + ChunkUtil.buildChunkTempFileName(chunk);
            // 开始上锁
            Long lockNum = RedisFileHelper.getInstance().tryChunkLock(taskId, chunk);
            if (lockNum == 1) {
                // 获得锁,开始执行
                if (FileUtil.exist(tempFilePath)) {
                    log.error("此线程={}将覆盖写入(删除->新建)，文件[{}]分片[{}]", Thread.currentThread().getId(), taskId, chunk);
                    FileUtil.del(tempFilePath);
                }
                tempFile = FileUtil.touch(tempFilePath);
                filePart.transferTo(tempFile)
                        .doOnSuccess(x -> {
                            log.debug("block={}上传完毕, byte length ={}", chunk, currentChunkSize);
                            // 释放锁
                            RedisFileHelper.getInstance().releaseChunkLock(taskId, chunk);
                        }).doOnError(err -> log.error(err.getLocalizedMessage())).subscribe();
            } else {
                // 没有锁（有线程在执行），直接退出
                log.error("文件[{}]分片[{}]被锁（redis分布式锁）,此线程={}不写入", taskId, chunk, Thread.currentThread().getId());
                return;
            }
        } catch (Exception e) {
            log.error("分块[{}]文件上传失败,错误信息={}", chunk, e.toString());
            throw new BusinessException(StrUtil.format("分块[{}]文件上传失败", chunk));
        }
    }

    /**
     * 获取本地chunk文件的md5
     *
     * @param id
     * @param chunk
     * @return
     */
    public String getChunkFileLocalMd5(String id, Integer chunk) {
        File backFile = FileUtil.touch(sysProperties.getTempRootPath(id) + File.separator + ChunkUtil.buildChunkTempFileName(chunk));
        try {
            return Md5Util.file2Md5(backFile);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 合并某个文件上传的所有分片
     *
     * @param taskId
     */
    @LogId
    public void newMerge(String taskId, boolean failTaskNeedPutRedisQueue) {
        // 文件存在，不要合并 fixme 并非多此一举，规避前端未控制好，导致多次异常合并，导致程序非正常执行，日志凌乱
        String existArchiveFilePath = this.getArchiveFile(taskId);
        if (StrUtil.isNotBlank(existArchiveFilePath)) {
            log.error("fixme 进入此处不合理，多次合并请求，这是不正常的");
            return;
        }
        TaskDto taskDto = RedisFileHelper.getInstance().getTaskById(taskId);
        Set<Integer> allChunks = new HashSet<>(taskDto.getChunkMd5Modules().stream().map(ChunkMd5Module::getChunkNumber).collect(Collectors.toSet()));
        retryArchiveFileMd5Check(taskDto, allChunks, 10, 3, true);
    }

    /**
     * 重试归档文件md5校验
     *
     * @param taskDto
     * @param waitTime 重试间隔时间，单位为秒，缺省值为1
     * @param attempt  重试次数，单位为次，缺省值为3
     * @Return void
     * @Author lys
     * @Date 2020-6-10 10:48
     */
    public void retryArchiveFileMd5Check(TaskDto taskDto, Set<Integer> chunks, Integer waitTime, Integer attempt, boolean failTaskNeedPutRedisQueue) {
        waitTime = ObjectUtil.isNull(waitTime) ? 20 : waitTime;
        attempt = ObjectUtil.isNull(attempt) ? 3 : attempt;
        // 错误重试
        Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                //抛出runtime异常、checked异常时都会重试，但是抛出error不会重试。
                .retryIfException()
                //返回false也需要重试
                .retryIfResult(result -> Objects.equals(result, false))
                //重调策略
                .withWaitStrategy(WaitStrategies.fixedWait(waitTime, TimeUnit.SECONDS))
                //尝试次数
                .withStopStrategy(StopStrategies.stopAfterAttempt(attempt))
                .build();
        try {
            retryer.call(() -> {
                boolean checkChunkAllOk = RedisFileHelper.getInstance().checkChunkAllOk(taskDto.getId());
                if (checkChunkAllOk) {
                    log.debug("所有分块都上传完了");
                    if (chunkMd5CheckPass(taskDto, chunks)) {
                        // 发送文件上传完毕事件
                        ChunkMergeEvent chunkMergeEvent = new ChunkMergeEvent(this, taskDto, failTaskNeedPutRedisQueue);
                        try {
                            applicationEventPublisher.publishEvent(chunkMergeEvent);
                        } catch (Exception e) {
                            log.error(e.toString());
                        }
                        return true;
                    }
                }
                return false;
            });
        } catch (ExecutionException e) {
            log.error("ExecutionException e 重试执行retryArchiveFileMd5Check,taskDto={}execute出现异常,错误消息={}", taskDto, e.getMessage());
        } catch (RetryException e) {
            log.error("RetryException e 重试执行retryArchiveFileMd5Check,taskDto={},retry出现异常,错误消息={}", taskDto, e.getMessage());
        } catch (com.github.rholder.retry.RetryException e) {
            log.error("重试错误信息={}", e.toString());
        }
    }


    /**
     * md5校验
     *
     * @param identify
     * @param archiveFile
     * @return
     */
    public boolean archiveMd5CheckPass(String identify, File archiveFile) {
        try {
            if (!FileUtil.exist(archiveFile)) {
                return false;
            }
            String md5String = Md5Util.file2Md5(archiveFile);
            FileHelper.getInstance().writeMd5File(md5String, archiveFile.getParent() + File.separator + "md_" + identify + ".txt");
            if (!StrUtil.equals(md5String, identify)) {
                return false;
            }
        } catch (FileNotFoundException e) {
            log.error("文件[{}]的md5校验出错,可能是异步存储还未执行完成，文件未找到，具体错误信息={}", identify, e.toString());
            return false;
        } catch (IOException | NoSuchAlgorithmException e) {
            log.error("文件[{}]的md5校验出错，具体错误信息={}", identify, e.toString());
            return false;
        }
        return true;
    }


    /**
     * 分块校验
     *
     * @param taskDto
     * @param chunks
     * @return
     * @throws BusinessException
     */
    public boolean chunkMd5CheckPass(TaskDto taskDto, Set<Integer> chunks) throws BusinessException {
        for (Integer chunk : chunks) {
            if (!chunkMd5CheckPass(taskDto, chunk)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 分块校验
     *
     * @param taskDto
     * @param chunk
     * @return
     * @throws BusinessException
     */
    public boolean chunkMd5CheckPass(TaskDto taskDto, Integer chunk) throws BusinessException {
        try {
            File chunkFile = FileUtil.touch(sysProperties.getTempRootPath(taskDto.getId()) + File.separator + ChunkUtil.buildChunkTempFileName(chunk));
            List<ChunkMd5Module> filterList = taskDto.getChunkMd5Modules().stream().filter(item -> item.getChunkNumber() == chunk).collect(Collectors.toList());
            if (CollUtil.isNotEmpty(filterList)) {
                ChunkMd5Module chunkMd5Module = filterList.get(0);
                String preSysChunkMd = chunkMd5Module.getMd5();
                String fileChunkMd = Md5Util.file2Md5(chunkFile);
                FileHelper.getInstance().writeMd5File(fileChunkMd, sysProperties.getTempRootPath(taskDto.getIdentify()) + File.separator + ChunkUtil.buildLocalMd5(chunk));
                log.debug("taskId={} chunk({})---{}----{}", taskDto.getId(), chunkMd5Module.getChunkNumber(), preSysChunkMd, fileChunkMd);
                if (!StrUtil.equals(preSysChunkMd, fileChunkMd)) {
                    return false;
                }
            } else {
                log.error("一般不会进入，进入则存在bug,当前taskid={},chunk={}", taskDto.getId(), chunk);
            }
        } catch (Exception e) {
            log.error("分块[{}]文件的md5校验出错，错误信息={}", chunk, e.toString());
            return false;
        }
        return true;
    }

    /**
     * 构建附件
     *
     * @param taskDto
     * @return
     */
    public Attachment buildAttachment(TaskDto taskDto) {
        Attachment attachment = new Attachment();
        attachment.setId(IdUtil.fastUUID());
        attachment.setBizId("");
        attachment.setBizTypeCode("");
        attachment.setBizTypeName("");
        attachment.setBizExtra(null);
        attachment.setDataType(DataType.OTHER);
        attachment.setSubmittedFileName(taskDto.getFilename());
        attachment.setRelativePath(sysProperties.getArchivePath(taskDto.getUploadTime(), taskDto.getId()).toString());
        attachment.setUrl("");
        attachment.setFileMd5(taskDto.getMd5());
        attachment.setContextType("");
        attachment.setFilename(taskDto.getFilename());
        attachment.setExt(FileUtil.getSuffix(taskDto.getFilename()));
        attachment.setSize(taskDto.getTotalSize());
        attachment.setOrgId("");
        attachment.setIcon(IconType.getIcon(FileUtil.getSuffix(taskDto.getFilename())).getIcon());
        setDate(attachment);
        setCommonData(attachment);
        return attachment;
    }

    /**
     * 设置通用数据
     *
     * @param attachment
     */
    private void setCommonData(Attachment attachment) {
        Date now = new Date();
        attachment.setCreateTime(now);
        attachment.setCreateUser("");
        attachment.setUpdateTime(now);
        attachment.setUpdateUser("");
    }

    /**
     * 设置归档时间
     *
     * @param file
     */
    private void setDate(Attachment file) {
        LocalDateTime now = LocalDateTime.now();
        file.setCreateMonth(DateUtil.format(now, "yyyy年MM月"));
        file.setCreateWeek(DateUtil.format(now, "yyyy年ww周"));
        file.setCreateDay(DateUtil.format(now, "yyyy年MM月dd日"));
    }

    /**
     * 查询已经上传过的chunk，判断标准：redis与本地都存在
     *
     * @param taskId
     * @return
     */
    public List<Integer> getExistChunkNums(String taskId) {
        TaskDto taskDto = RedisFileHelper.getInstance().getTaskById(taskId);
        File[] existFileChunks = ObjectUtil.isNotNull(FileHelper.getInstance().getExistChunks(taskId)) ? FileHelper.getInstance().getExistChunks(taskId) : new File[0];
        return Arrays.stream(existFileChunks).filter(item -> chunkMd5CheckPass(taskDto, Integer.valueOf(StrUtil.split(item.getName(), ".")[0]))).map(item -> Integer.valueOf(StrUtil.split(item.getName(), ".")[0])).collect(Collectors.toList());
    }

    /**
     * 获取归档文件（从db与本地文件存储中获取),存在返回相对地址，否则返回空
     *
     * @param fileMd5
     * @return
     */
    public String getArchiveFile(String fileMd5) {
        Attachment attachment = attachmentService.queryFile(fileMd5);
        if (ObjectUtil.isNotNull(attachment)) {
            if (FileHelper.getInstance().existArchive(attachment.getUpdateTime(), fileMd5)) {
                return attachment.getRelativePath();
            }
        }
        return null;
    }

    /**
     * 清空一个任务
     *
     * @param uploadTime
     * @param identify
     */
    public void clearTaskWithFileAndRedis(Date uploadTime, String identify) {
        // 1) 清空本地存储
        FileHelper.getInstance().clearAllAboutFile(uploadTime, identify);
        // 2）清空redis临时信息存储
        RedisFileHelper.getInstance().clearTaskAndChunks(identify);
    }

    /**
     * 开始文件上传预处理
     *
     * @param startUploadReq
     * @return
     */
    @Transactional
    public Mono<Result> startFileChunkUploadPreHandle(StartUploadReq startUploadReq) {
        String fileMd5 = startUploadReq.getFileMd5();
        List<ChunkMd5Module> chunkMd5s = startUploadReq.getChunkMd5s();
        int chunkSize = startUploadReq.getChunkSize();
        String identify = startUploadReq.getIdentify();
        long totalSize = startUploadReq.getTotalSize();
        int totalChunk = startUploadReq.getTotalChunk();
        String filename = startUploadReq.getFilename();
        if (startUploadReq.isCover()) {
            // 覆盖则完全重试
            Attachment attachment = attachmentService.queryFile(identify);
            if (ObjectUtil.isNotNull(attachment)) {
                // 清空文件与redis
                clearTaskWithFileAndRedis(attachment.getUpdateTime(), identify);
            } else {
                // 清空redis临时信息存储
                RedisFileHelper.getInstance().clearTaskAndChunks(identify);
                // 清空本地临时文件
                FileHelper.getInstance().clearTemp(identify);
            }
            // 清空附件DB存储
            attachmentService.removeAttachment(identify);
            // 清空临时附件DB存储
            attachmentChunkTempService.removeAttachmentTemp(identify);
        } else {
            // 续传则存在续传
            if (FastUploadHelper.getInstance().isExistArchiveWithDbAndLocal(identify)) {
                return Mono.just(Result.ok(TaskStatusEnum.FILE_ALREADY_EXIST, null));
            }
        }
        // 曾经上传过一半，续传
        AttachmentChunkTemp existAttachmentChunkTemp = attachmentChunkTempService.queryByFileId(identify);
        if (ObjectUtil.isNotNull(existAttachmentChunkTemp)) {
            Integer needSize = existAttachmentChunkTemp.getChunkSize();
            // 发现分块大小变更了
            if (startUploadReq.getChunkSize() != needSize) {
                return Mono.just(Result.ok(TaskStatusEnum.MUST_RESTART, null));
            }
        }
        Date now = new Date();
        TaskDto taskDto = new TaskDto();
        taskDto.setId(identify);
        taskDto.setFilename(filename);
        taskDto.setMd5(fileMd5);
        taskDto.setIdentify(identify);
        taskDto.setTotalChunk(totalChunk);
        taskDto.setTotalSize(totalSize);
        taskDto.setChunkSize(chunkSize);
        taskDto.setUploadTime(now);
        taskDto.setStatus(TaskStatusEnum.CREATE.getCode());
        taskDto.setChunkMd5Modules(chunkMd5s);
        taskDto.setUserId(startUploadReq.getUserId());
        taskDto.setRelativePath(sysProperties.getArchivePath(now, identify).getPath());
        RedisFileHelper.getInstance().saveTaskOrUpd(taskDto);
        if (ObjectUtil.isNull(existAttachmentChunkTemp)) {
            AttachmentChunkTemp attachmentChunkTemp = AttachmentChunkTemp.builder().fileMd5(taskDto.getIdentify()).chunkSize(taskDto.getChunkSize())
                    .lastChunkSize((int) (startUploadReq.getTotalSize() % startUploadReq.getChunkSize())).totalChunk(taskDto.getTotalChunk()).size(taskDto.getTotalSize()).build();
            attachmentChunkTempService.saveOrUpdateAttachmentChunkTemp(attachmentChunkTemp);
        }
        taskDto.setChunkMd5Modules(new ArrayList<>());
        return Mono.just(Result.ok(TaskStatusEnum.CREATE, taskDto));
    }
}
